今日目標,加入各種限制來完善註冊功能。
我們定義註冊必須滿足一些條件:Email、Username 必須唯一,而且 Password 長度不能少於 8
<!-- Validation -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
package com.example.user;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
@Entity
@Table(name = "user")
@Getter @Setter
public class UserModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true)
@NotBlank(message = "帳號不可為空")
private String username;
@Column(unique = true)
@Email(message = "信箱格式錯誤")
@NotBlank(message = "信箱不可為空")
private String email;
@Column
@Size(min = 8, message = "密碼不可少於8位")
@NotBlank(message = "密碼不可為空")
private String password;
}
@Email
:聲明該欄位必須為 Email 的格式@NotBlank
:聲明該欄位不能空白@Size
:聲明該欄位的大小(min, max)package com.example.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import java.util.Set;
@Service
@Validated
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private Validator validator;
public Integer addUser(UserModel user) {
Set<ConstraintViolation<UserModel>> violations = validator.validate(user);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<UserModel> constraintViolation : violations) {
sb.append(constraintViolation.getMessage());
}
throw new ConstraintViolationException(sb.toString(), violations);
}
UserModel newUser = userRepository.save(user);
return newUser.getId();
}
}
validator.validate
檢驗資料,當有欄位不通過檢驗,就會將其蒐集,並透過 ConstraintViolationException
來將錯誤訊息以及錯誤本身丟出,而丟出的 ConstraintViolationException
將會在 controller 捕獲並處理registerProcess
,檔案完整內容為:
package com.example.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.validation.Valid;
import java.util.Objects;
@Controller
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/register")
public String viewRegisterPage(Model model) {
model.addAttribute("name", "註冊");
model.addAttribute("user", new UserModel());
return "register";
}
@PostMapping("/register")
public String registerProcess(@Valid UserModel user, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
String message = Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage();
redirectAttributes.addFlashAttribute("error", message);
return "redirect:/register";
}
userService.addUser(user);
return "redirect:/";
}
}
registerProcess
的參數 BindingResult bindingResult
將會負責捕獲錯誤,藉由 bindingResult.hasErrors()
來確認是否有錯誤被捕獲,如果有就使用 bindingResult.getFieldError().getDefaultMessage()
來取得錯誤訊息registerProcess
的參數 RedirectAttributes redirectAttributes
用來傳遞一次性的重導向參數,當使用者輸入的資訊未通過檢驗,將其重新導向註冊頁面,並給予訊息提示,而訊息就來自 redirectAttributes.addFlashAttribute("error", message)
的設定<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${name}"></h1>
<div th:if="${error}">
<div th:text="${error}"></div>
</div>
<form method="post" action="/register" th:object="${user}">
<input type="email" id="email" name="email" placeholder="Email" th:field="*{email}" />
<input type="text" id="username" name="username" placeholder="Username" th:field="*{username}" />
<input type="password" id="password" name="password" placeholder="Password" th:field="*{password}" />
<button type="submit">註冊</button>
</form>
</body>
</html>
th:if="${error}"
:當 error 變數存在時,就顯示出錯誤訊息我們如果要擋掉重複的 Email 或 Username 註冊,一種做法是自己定義一個新的驗證約束註解(validation constraint annoation),另一種是捕獲資料庫的 ConstraintViolationException
,小弟我試了很多次,發現實在捕獲不到資料庫的錯誤,所以就自己定義規則囉,而且這樣也比較容易,不必再新增 Service 驗證,只需要原本的 Validator 即可
findUserByEmail
和 findUserByUsername
,完整檔案內容為:
package com.example.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import java.util.Set;
@Service
@Validated
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private Validator validator;
public UserModel findUserByEmail(String email) {
return userRepository.findByEmail(email);
}
public UserModel findUserByUsername(String username) {
return userRepository.findByUsername(username);
}
public Integer addUser(UserModel user) {
Set<ConstraintViolation<UserModel>> violations = validator.validate(user);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<UserModel> constraintViolation : violations) {
sb.append(constraintViolation.getMessage());
}
throw new ConstraintViolationException(sb.toString(), violations);
}
UserModel newUser = userRepository.save(user);
return newUser.getId();
}
}
package com.example.user;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
String message() default "此信箱已被使用,請換一組";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
@Constraint
:透過 validatedBy 來告知使用什麼驗證器(validator)來作驗證
@Target
:決定註解聲明的對象和範圍,由 ElementType 來定義
@Retention
:決定這個註解聲明存在的生命週期,由 RetentionPolicy 來定義
package com.example.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired
private UserService userService;
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
return (userService.findUserByEmail(email) == null);
}
}
ConstraintValidator
來實作驗證器,而後方的參數第一個是放註解類,第二個則是要檢驗的資料類型,所以這邊表示我們用 UniqueEmail
註釋來檢驗 輸入的 email(String
)isValid
來實現自定義檢驗規則,這邊是當我無法用 email 找到 user(就是 null)表示合法,因為我們不希望有 email 重複package com.example.user;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Constraint(validatedBy = UniqueUsernameValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueUsername {
String message() default "此帳號已被使用,請換一組";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
package com.example.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
@Component
public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {
@Autowired
private UserService userService;
@Override
public boolean isValid(String username, ConstraintValidatorContext context) {
return (userService.findUserByUsername(username) == null);
}
}
package com.example.user;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
@Entity
@Table(name = "user")
@Getter @Setter
public class UserModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true)
@NotBlank(message = "帳號不可為空")
@UniqueUsername
private String username;
@Column(unique = true)
@Email(message = "信箱格式錯誤")
@NotBlank(message = "信箱不可為空")
@UniqueEmail
private String email;
@Column
@Size(min = 8, message = "密碼不可少於8位")
@NotBlank(message = "密碼不可為空")
private String password;
}